<?php

declare(strict_types=1);

namespace App\Exception\Handler;

use App\Constants\ResponseCode;
use App\Exception\BusinessException;
use Hyperf\Codec\Json;
use Hyperf\Database\Model\ModelNotFoundException;
use Hyperf\HttpMessage\Exception\HttpException;
use Hyperf\HttpMessage\Exception\MethodNotAllowedHttpException;
use Hyperf\HttpMessage\Exception\NotFoundHttpException;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Logger\LoggerFactory;
use Hyperf\Validation\ValidationException;
use Mine\Exception\TokenException;
use Mine\Helper\MineCode;
use Mine\Log\RequestIdHolder;
use Psr\Http\Message\ResponseInterface;
use Throwable;

use function Hyperf\Config\config;

class AppExceptionHandler extends \Mine\Exception\Handler\AppExceptionHandler
{
    private string $appName;
    private bool $isProd;

    public function __construct()
    {
        $this->appName = (string)config('app_name');
        $this->isProd = config('app_env') === 'prod';
        $this->console = console();
        $this->logger = container()->get(LoggerFactory::class)->get($this->appName);
    }

    public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
    {
        // 格式化异常
        [$status, $result, $isRecodeLog] = $this->format($throwable);

        // 控制台输出
        if (!$this->isProd) {
            $this->console->error(sprintf("%s[%s] in %s\n%s", $throwable->getMessage(), $throwable->getLine(), $throwable->getFile(), $throwable->getTraceAsString()));
        }

        // 记录日志
        if ($isRecodeLog) {
            $this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()), $throwable->getTrace());
        }

        // 返回数据
        return $response->withHeader('Server', $this->appName)
            ->withHeader('Access-Control-Allow-Origin', '*')
            ->withHeader('Access-Control-Allow-Methods','GET,PUT,POST,DELETE,OPTIONS')
            ->withHeader('Access-Control-Allow-Credentials', 'true')
            ->withHeader('Access-Control-Allow-Headers', 'accept-language,authorization,lang,uid,token,Keep-Alive,User-Agent,Cache-Control,Content-Type,time,x-lang,version,set-token')
            ->withAddedHeader('content-type', 'application/json; charset=utf-8')
            ->withStatus($status)
            ->withBody(new SwooleStream(Json::encode($result)));
    }

    /**
     * 格式化异常
     * @param Throwable $throwable
     * @return array [$status, $result, $isRecodeLog]
     */
    private function format(Throwable $throwable): array
    {
        // 是否接口请求
        $isApi = is_api();

        // 是否记录日志
        $isRecodeLog = true;
        $status = $isApi ? 200 : 500;
        $code = $isApi ? ResponseCode::ERROR_DEFAULT : 500;
        $message = $throwable->getMessage();
        $data = null;

        switch (true) {
            // 请求异常
            case $throwable instanceof HttpException:
                if ($throwable instanceof NotFoundHttpException || $throwable instanceof MethodNotAllowedHttpException) {
                    $isRecodeLog = false;
                }
                if ($isApi) {
                    $code = $throwable->getStatusCode();
                    $message = ResponseCode::getMessage($code);
                } else {
                    $status = $throwable->getStatusCode();
                }
                break;
            // 业务异常
            case $throwable instanceof BusinessException && $isApi:
                $isRecodeLog = false;
                $code = $throwable->getCode();
                $data = $throwable->getData();
                break;
            // 字段验证异常
            case $throwable instanceof ValidationException:
                $isRecodeLog = false;
                $status = 200;
                $code = $isApi ? ResponseCode::ERROR_VALIDATION_FAILED : MineCode::VALIDATE_FAILED;
                $message = $throwable->validator->errors()->first();
                break;
            // 模型不存在异常
            case $throwable instanceof ModelNotFoundException && $isApi:
                $isRecodeLog = false;
                $code = ResponseCode::ERROR_DATA_NOT_FOUND;
                $message = ResponseCode::getMessage($code);
                break;
            // 令牌异常
            case $throwable instanceof TokenException:
                $isRecodeLog = false;
                if ($isApi) {
                    $code = ResponseCode::HTTP_UNAUTHORIZED;
                    $message = ResponseCode::getMessage($code);
                } else {
                    $status = 401;
                    $code = MineCode::TOKEN_EXPIRED;
                }
                break;
        }

        // 返回数据
        if ($isApi) {
            $result = [
                'code' => $code,
                'msg' => $message,
                'data' => (object)$data,
            ];
        } else {
            $result = [
                'requestId' => RequestIdHolder::getId(),
                'success' => false,
                'code'    => $code,
                'message' => $message,
            ];
        }

        // 调用栈
        if (!$this->isProd) {
            $result['trace'] = $throwable->getTrace();
        }

        return [$status, $result, $isRecodeLog];
    }
}
